﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DatabaseSimulation
{
    public class RestrictivePresedenceDatabase : Database
    {
        private int WaitingKills = 0;
        private int PrcedenceKills = 0;
        private int ExclusiveBlockKills = 0;

        public int ReadPhaseTimeout1 { get; private set; }

        public int ReadPhaseTimeout2 { get; private set; }

        public int WaitToCommitTimeout { get; private set; }

        //The key must precede the value
        private List<KeyValuePair<Transaction, List<Transaction>>> PrecedenceGraph { get; set; }

        private List<Transaction> BeforeTransactions
        {
            get
            {
                return this.PrecedenceGraph.ConvertAll(i => i.Key);
            }
        }

        private List<Transaction> AfterTransactions
        {
            get
            {
                return this.PrecedenceGraph.SelectMany(i => i.Value).ToList();
            }
        }

        public RestrictivePresedenceDatabase(TransactionGenerator transactionGenerator, int numberOfObjects, int numberOfHardDrives, int numberOfCpus, int milliseconds, int readPhaseTimeout1, int readPhaseTimeout2, int waitToCommitTimeout) : base(transactionGenerator, numberOfObjects, numberOfHardDrives, numberOfCpus, milliseconds)
        {
            this.ReadPhaseTimeout1 = readPhaseTimeout1;
            this.ReadPhaseTimeout2 = readPhaseTimeout2;
            this.WaitToCommitTimeout = waitToCommitTimeout;
            this.PrecedenceGraph = new List<KeyValuePair<Transaction, List<Transaction>>>();
        }

        protected override bool ReadAfterWriteConflict(DatabaseOperation operation)
        {
            bool canContinue;

            DatabaseObject dbObject = this.DatabaseObjects[operation.ObjectID];
            List<Transaction> before = new List<Transaction>() { operation.Transaction };
            List<Transaction> after = dbObject.WritingTransactions.FindAll(i => i != operation.Transaction);

            if (IsPrudentToAddPrecedence(before, after))
            {
                this.AddPrecedence(before, after);
                canContinue = true;
            }
            else
            {
                this.HandleBlockedOperation(operation);
                canContinue = false;
            }

            return canContinue;
        }

        protected override bool WriteAfterReadConflict(DatabaseOperation operation)
        {
            bool canContinue;

            DatabaseObject dbObject = this.DatabaseObjects[operation.ObjectID];
            List<Transaction> before = dbObject.ReadingTransactions.FindAll(i => i != operation.Transaction);
            List<Transaction> after = new List<Transaction>() { operation.Transaction };

            if (IsPrudentToAddPrecedence(before, after))
            {
                this.AddPrecedence(before, after);
                canContinue = true;
            }
            else
            {
                this.HandleBlockedOperation(operation);
                canContinue = false;
            }

            return canContinue;
        }

        protected override bool CheckCanCommitTransaction(Transaction transaction)
        {
            bool obtainedExclusiveLocks = HasExclusiveLocks(transaction) || ObtainExclusiveLocks(transaction);
            bool noPrecedingTransactions = TransactionCanCommit(transaction);
            bool result = false;

            if (obtainedExclusiveLocks && noPrecedingTransactions)
            {
                result = true;
            }
            else if (!obtainedExclusiveLocks)
            {
                if (transaction.ElapsedWaitOnObjetsToCommit == 0)
                {
                    this.CurrentSimulationResult.HardDriveWriteBlocks++;
                }
                transaction.ElapsedWaitOnObjetsToCommit++;
                this.CurrentSimulationResult.ElapsedWaitOnObjectsToCommit++;
            }
            else
            {
                if (transaction.ElapsedWaitOnTransactionsToCommit == 0)
                {
                    this.CurrentSimulationResult.TransactionBlocks++;
                }
                transaction.ElapsedWaitOnTransactionsToCommit++;
                this.CurrentSimulationResult.ElapsedWaitOnTransactionsToCommit++;

                if (transaction.ElapsedWaitOnTransactionsToCommit == this.WaitToCommitTimeout)
                {
                    this.WaitingKills += this.PrecedenceGraph.First(i => i.Key == transaction).Value.Count;
                    this.PrecedenceGraph.First(i => i.Key == transaction).Value.ForEach(i => KillTransaction(i));
                }
            }

            return result;
        }

        private bool ObtainExclusiveLocks(Transaction transaction)
        {
            bool result = false;

            List<DatabaseObject> objects = GetObjectsForExclusiveLocks(transaction);

            if (objects.All(i => i.ExclusiveTransaction == null))
            {
                objects.FindAll(i => i.ExclusiveTransaction == null).ForEach(i => i.SetExclusiveTransaction(transaction));
                result = objects.All(i => i.ExclusiveTransaction == transaction);
            }

            //if (!result)
            //{
            //    throw new Exception("Could not obtain all exclusive locks");
            //}

            return result;
        }

        private bool HasExclusiveLocks(Transaction transaction)
        {
            List<DatabaseObject> objects = GetObjectsForExclusiveLocks(transaction);
            return objects.All(i => i.ExclusiveTransaction == transaction);
        }

        protected override void TransactionCommitted(Transaction transaction)
        {
            List<DatabaseObject> objects = GetObjectsForExclusiveLocks(transaction);
            objects.ForEach(i => i.ClearExclusiveTransaction(transaction));

            this.PrecedenceGraph.RemoveAll(i => i.Key == transaction);
            this.PrecedenceGraph.ConvertAll(i => i.Value).ForEach(i => i.Remove(transaction));
        }

        private List<DatabaseObject> GetObjectsForExclusiveLocks(Transaction transaction)
        {
            List<DatabaseOperation> operations = transaction.Operations.FindAll(i => i.OperationType == DatabaseOperationType.Write);
            return operations.ConvertAll(i => this.DatabaseObjects[i.ObjectID]).Distinct().ToList();
        }

        protected override void HandleExclusiveBlock(DatabaseOperation operation)
        {
            HandleBlockedOperation2(operation);
        }

        private void HandleBlockedOperation(DatabaseOperation operation)
        {
            if (operation.ElapsedBlockedOnTransactionTime == 0)
            {
                this.CurrentSimulationResult.TransactionBlocks++;
            }
            operation.IncreaseBlockedOnTransactionTime();
            this.CurrentSimulationResult.BlockedOnTransactionTime++;

            if (operation.ElapsedBlockedOnTransactionTime == this.ReadPhaseTimeout1)
            {
                this.PrcedenceKills += 1;
                KillTransaction(operation.Transaction);
            }
        }

        private void HandleBlockedOperation2(DatabaseOperation operation)
        {
            if (operation.ElapsedBlockedOnTransactionTime2 == 0)
            {
                this.CurrentSimulationResult.TransactionBlocks++;
            }
            operation.IncreaseBlockedOnTransactionTime2();
            this.CurrentSimulationResult.BlockedOnTransactionTime++;

            if (operation.ElapsedBlockedOnTransactionTime2 == this.ReadPhaseTimeout2)
            {
                this.ExclusiveBlockKills += 1;
                KillTransaction(operation.Transaction);
            }
        }

        private bool IsPrudentToAddPrecedence(List<Transaction> before, List<Transaction> after)
        {
            return !this.AfterTransactions.Intersect(before).Any() && !this.BeforeTransactions.Intersect(after).Any();
        }

        private void AddPrecedence(List<Transaction> before, List<Transaction> after)
        {
            foreach (Transaction transaction in before)
            {
                this.AddPrecedence(transaction, after);
            }
        }

        private void AddPrecedence(Transaction before, List<Transaction> after)
        {
            KeyValuePair<Transaction, List<Transaction>> entry;
            if (this.PrecedenceGraph.Any(i => i.Key == before))
            {
                entry = this.PrecedenceGraph.FirstOrDefault(i => i.Key == before);
                entry.Value.AddRange(after.Except(entry.Value));
            }
            else
            {
                entry = new KeyValuePair<Transaction, List<Transaction>>(before, after.ToList());
                this.PrecedenceGraph.Add(entry);
            }
        }        

        private bool TransactionCanCommit(Transaction transaction)
        {
            return !this.PrecedenceGraph.Any(i => i.Key == transaction && i.Value.Any());
        }

        protected override void SimulationComplete(int mpl)
        {
            System.Diagnostics.Debug.WriteLine("");
            System.Diagnostics.Debug.WriteLine("Results for " + mpl + ":");
            System.Diagnostics.Debug.WriteLine("Waiting Kills " + this.WaitingKills);
            System.Diagnostics.Debug.WriteLine("Prcedence Kills " + this.PrcedenceKills);
            System.Diagnostics.Debug.WriteLine("ExclusiveBlock Kills " + this.ExclusiveBlockKills);
        }
    }
}
